﻿using System;
using System.Collections.Generic;
using UnityEngine;
using Utility.Math;
using System.Linq;
using M = System.Math;

namespace Utility.Random
{
	public class UnityRandom : IRandomSource
	{
		static UnityRandom _inst;
		public static UnityRandom Inst => _inst ?? (_inst = new UnityRandom ());

		double IRandomSource.Sample ()
		{
			float val;
			do
			{
				val = UnityEngine.Random.value;
			} while (val == 1);
			return val;
		}
	}

	public class SystemRandom : IRandomSource
	{
		static SystemRandom _inst;
		public static SystemRandom Inst => _inst ?? (_inst = new SystemRandom ());

		System.Random rnd;

		public SystemRandom ()
		{
			rnd = new System.Random ();
		}
		public SystemRandom (int seed)
		{
			rnd = new System.Random (seed);
		}
		public SystemRandom (System.Random rnd)
		{
			this.rnd = rnd;
		}

		double IRandomSource.Sample ()
		{
			return rnd.NextDouble ();
		}
	}

	public static class RandomExtension
	{
		#region simple
		public static float GetFloat (this IRandomSource rnd)
		{
			return (float)rnd.Sample ();
		}

		public static float GetFloat (this IRandomSource rnd, float from, float to)
		{
			return from + rnd.GetFloat () * (to - from);
		}

		/// <summary>
		/// random [from, to]
		/// </summary>
		/// <param name="rnd"></param>
		/// <param name="from"></param>
		/// <param name="to"></param>
		/// <returns></returns>
		public static int GetInt (this IRandomSource rnd, int from, int to)
		{
			return from + (int)(rnd.Sample () * (1 + to - from));
		}

		/// <summary>
		/// return true if rnd value < trueChance
		/// </summary>
		/// <param name="rnd"></param>
		/// <param name="trueChance"></param>
		/// <returns></returns>
		public static bool GetBool (this IRandomSource rnd, double trueChance = 0.5)
		{
			return rnd.Sample () < trueChance;
		}

		public static int GetSign (this IRandomSource rnd, double posChance = 0.5)
		{
			return rnd.GetBool (posChance) ? 1 : -1;
		}

		public static Vector2 GetVector2 (this IRandomSource rnd, Vector2 from, Vector2 to)
		{
			return new Vector2 (rnd.GetFloat (from.x, to.x), rnd.GetFloat (from.y, to.y));
		}

		public static Vector3 GetVector3 (this IRandomSource rnd, Vector3 from, Vector3 to)
		{
			return new Vector3 (rnd.GetFloat (from.x, to.x), rnd.GetFloat (from.y, to.y), rnd.GetFloat (from.z, to.z));
		}


		#endregion
		#region collection

		#region weighted
		/// <summary>
		/// thresholds should be a increasing list of floats that in range (0, thresholds[count - 1])
		/// </summary>
		/// <param name="rnd"></param>
		/// <param name="thresholds"></param>
		/// <returns></returns>
		public static int GetIndexFromThresholds (this IRandomSource rnd, IList<float> thresholds)
		{
			if (thresholds.Count <= 0)
			{
				return -1;
			}

			float val = rnd.GetFloat (0, thresholds[thresholds.Count - 1]);
			for (int i = 0; i < thresholds.Count; i++)
			{
				if (val < thresholds[i])
				{
					return i;
				}
			}
			return -1;
		}

		public static int GetWeightedIndex (this IRandomSource rnd, IList<float> weights)
		{
			if (weights.Count <= 0)
			{
				return -1;
			}

			using (var buffer = BufferList<float>.Spawn ())
			{
				//get thresholds
				float threshold = 0;
				for (int i = 0; i < weights.Count; i++)
				{
					threshold += weights[i];
					buffer.Add (threshold);
				}
				//get index
				int index = rnd.GetIndexFromThresholds (buffer);
				return index;
			}
		}

		public static T GetWeightedElement<T> (this IRandomSource rnd, int count, Func<int, T> elementGetter_FromIndex, Func<int, float> weightGetter_FromIndex)
		{
			if (count <= 0)
			{
				return default;
			}

			using (var buffer = BufferList<float>.Spawn ())
			{
				//get thresholds
				float threshold = 0;
				for (int i = 0; i < count; i++)
				{
					threshold += weightGetter_FromIndex (i);
					buffer.Add (threshold);
				}

				//get index
				int index = rnd.GetIndexFromThresholds (buffer);

				return elementGetter_FromIndex (index);
			}
		}

		public static T GetWeightedElement<T> (this IRandomSource rnd, IList<T> elements, IList<float> weights)
		{
			if (elements.Count <= 0)
			{
				return default;
			}

			using (var buffer = BufferList<float>.Spawn ())
			{
				//get thresholds
				float threshold = 0;
				for (int i = 0; i < weights.Count; i++)
				{
					threshold += weights[i];
					buffer.Add (threshold);
				}
				//get index
				int index = rnd.GetIndexFromThresholds (buffer);

				return elements.TryGet (index);
			}
		}

		public static T GetWeightedElement<T> (this IRandomSource rnd, IList<T> elements, Func<T, float> func_getWeightFromElement)
		{
			if (elements.Count <= 0)
			{
				return default;
			}

			using (var buffer = BufferList<float>.Spawn ())
			{
				//get thresholds
				float threshold = 0;
				for (int i = 0; i < elements.Count; i++)
				{
					float weight = func_getWeightFromElement (elements[i]);
					weight = weight < 0 ? 0 : weight;
					threshold += weight;
					buffer.Add (threshold);
				}

				//get index
				int index = rnd.GetIndexFromThresholds (buffer);

				return elements.TryGet (index);
			}
		}
		#endregion
		#endregion
		#region IList
		/// <summary>
		/// shuffle the list's segements of index from to to
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="rnd"></param>
		/// <param name="list"></param>
		/// <param name="from"></param>
		/// <param name="to"></param>
		public static void Shuffle<T> (this IRandomSource rnd, IList<T> list, int from = 0, int to = -1)
		{
			if (list == null || list.Count <= 1)
			{
				return;
			}

			if (from < 0)
			{
				throw new Exception ("from index must >=0");
			}
			to = to == -1 ? list.Count - 1 : to;
			if (to >= list.Count)
			{
				throw new Exception ("to index must <list.count");
			}
			if (to < from)
			{
				throw new Exception ("start index must > end index");
			}

			T temp;
			int index = 0;
			for (int i = from; i < to; i++)
			{
				index = GetInt (rnd, i, to);

				temp = list[index];
				list[index] = list[i];
				list[i] = temp;
			}

		}

		public static T GetElement<T> (this IRandomSource rnd, IList<T> list)
		{
			int index = rnd.GetInt (0, list.Count - 1);
			return list.Count == 0 ? default : list[index];
		}

		#endregion
		#region IEnumerable
		public static T GetElement<T> (this IRandomSource rnd, IEnumerable<T> enumerable)
		{
			var list = enumerable as IList<T> ?? enumerable.ToList ();
			return GetElement (rnd, list);
		}
		#endregion
		#region 2d position
		public static Vector2 GetPosition_UniformCircle (this IRandomSource rnd, float radius, Vector2 center = default)
		{
			if (radius <= 0)
			{
				return center;
			}

			float x = rnd.GetFloat (-radius, radius);
			float y = rnd.GetFloat (-radius, radius);
			if (x * x + y * y > radius * radius)
			{
				//out of circle 
				return rnd.GetPosition_UniformCircle (radius, center);
			}
			else
			{
				return center + new Vector2 (x, y);
			}
		}

		public static Vector2 GetPosition_UniformCircle (this IRandomSource rnd, Circle circle)
		{
			return rnd.GetPosition_UniformCircle (circle.radius, circle.center);
		}

		public static Vector2 GetPosition_UniformRectangle (this IRandomSource rnd, float width, float height,
			Vector2 center = default)
		{
			if (width <= 0 || height <= 0)
			{
				return center;
			}

			float x = rnd.GetFloat (-width / 2, width / 2);
			float y = rnd.GetFloat (-height / 2, height / 2);
			return center + new Vector2 (x, y);
		}

		public static Vector2 GetPosition_UniformSquare (this IRandomSource rnd, float sideLength,
			Vector2 center = default)
		{
			return rnd.GetPosition_UniformRectangle (sideLength, sideLength, center);
		}

		#endregion
		#region 3d position

		public static Vector3 GetPosition_UniformSphere (this IRandomSource rnd, float radius, Vector3 center = default)
		{
			if (radius <= 0)
			{
				return center;
			}

			float x = rnd.GetFloat (-radius, radius);
			float y = rnd.GetFloat (-radius, radius);
			float z = rnd.GetFloat (-radius, radius);
			if (x * x + y * y + z * z > radius * radius)
			{
				//out of circle 
				return rnd.GetPosition_UniformSphere (radius, center);
			}
			else
			{
				return center + new Vector3 (x, y, z);
			}
		}

		public static Vector3 GetPosition_UniformSphere (this IRandomSource rnd, Circle circle)
		{
			return rnd.GetPosition_UniformSphere (circle.radius, circle.center);
		}

		public static Vector3 GetPosition_UniformCuboid (this IRandomSource rnd, float width, float height, float length,
			Vector3 center = default)
		{
			if (width < 0 || height < 0 || length < 0)
			{
				return center;
			}

			float x = rnd.GetFloat (-width / 2, width / 2);
			float y = rnd.GetFloat (-height / 2, height / 2);
			float z = rnd.GetFloat (-length / 2, length / 2);
			return center + new Vector3 (x, y, z);
		}

		public static Vector3 GetPosition_UniformCuboid (this IRandomSource rnd, Vector3 size,
			Vector3 center = default)
		{
			return rnd.GetPosition_UniformCuboid (size.x, size.y, size.z, center);
		}

		public static Vector3 GetPosition_UniformCube (this IRandomSource rnd, float sideLength,
			Vector3 center = default)
		{
			return rnd.GetPosition_UniformCuboid (sideLength, sideLength, sideLength, center);
		}

		#endregion
		#region Gaussian value
		//Box-Muller tansform
		public static (double, double) StandardGaussianPair (this IRandomSource rnd)
		{
			double u, v, s;
			do
			{
				GetUV ();
			} while (s == 0 || s >= 1);

			var a = M.Sqrt (-2 * M.Log (s) / s);
			return (u * a, v * a);

			void GetUV ()
			{
				u = rnd.Sample () * 2 - 1;
				v = rnd.Sample () * 2 - 1;
				s = u * u + v * v;
			}
		}

		public static double Gaussian (this IRandomSource rnd, double mean = 0, double stdDev = 1)
		{
			var rndStdNormal = rnd.StandardGaussianPair ().Item1;
			return mean + stdDev * rndStdNormal;
		}

		public static float GaussianFloat (this IRandomSource rnd, float mean = 0, float stdDev = 1)
		{
			return (float)rnd.Gaussian (mean, stdDev);
		}

		public static Vector2 GaussianVector2 (this IRandomSource rnd, Vector2 mean = default, float stdDev = 1)
		{
			var (v0, v1) = rnd.StandardGaussianPair ();
			return mean + stdDev * new Vector2 ((float)v0, (float)v1);
		}
		#endregion
		#region linear weighted value
		/// <summary>
		/// return a random value that the weight is changed linear from w_0 at 0 to w_1 at 1
		/// </summary>
		/// <param name="rnd"></param>
		/// <param name="w_0"></param>
		/// <param name="w_1"></param>
		/// <returns></returns>
		public static double LinearWeighted (this IRandomSource rnd, double w_0, double w_1)
		{
			if (w_0 == w_1)
			{
				//same as uniform
				return rnd.Sample ();
			}
			else if (w_0 > w_1)
			{
				//mirror the value
				return 1 - rnd.LinearWeighted (w_1, w_0);
			}
			else if (w_0 < 0)
			{
				if (w_1 <= 0)
				{
					//same as uniform
					return rnd.Sample ();
				}
				else
				{
					var p = w_0 / (w_0 - w_1);
					return p + rnd.LinearWeighted (0, w_1) * (1 - p);
				}
			}
			else
			{
				var x = rnd.Sample ();
				var y = rnd.Sample () * (w_0 + w_1) / 2;

				var fx = w_0 + (w_1 - w_0) * x;
				if (x < 0.5 && y > fx)
				{
					x = 1 - x;
				}
				return x;
			}
		}

		public static float LinearWeightedFloat (this IRandomSource rnd, float w_0, float w_1, float from, float to)
		{
			return to + (float)rnd.LinearWeighted (w_0, w_1) * (from - to);
		}

		public static float LinearWeightedFloat (this IRandomSource rnd, Vector2 weight, Vector2 range)
		{
			return rnd.LinearWeightedFloat (weight.x, weight.y, range.x, range.y);
		}
		#endregion
	}
}